//
//  Parser
//
//  Created by Caue W. on 2011-04-03.
//  Copyright (c) 2011 __MyCompanyName__. All rights reserved.
//
package time.compiler.query;
#if macro
import haxe.macro.Map;
import time.compiler.types.TypeBuilder;

private typedef StdType = Type;

import time.compiler.query.Data;
import time.compiler.query.Solver;
import time.compiler.types.Data;
import haxe.macro.Expr;
import haxe.macro.Context;
import haxe.macro.Type;

using time.compiler.types.Data;

class Parser 
{
	#if macro
	
	static var tvoid = DBBasic(BVoid);
	static var tfunc = DBBasic(BFunction);
	static var tint = DBBasic(BInt);
	static function tvarchar(s) return DBBasic(BVarchar(s))
	static var ttext = DBBasic(BText);
	static var tdate = DBBasic(BDate);
	static var tint32 = DBBasic(BInt32);
	static var tint64 = DBBasic(BInt64);
	static var tbool = DBBasic(BBool);
	static var tdouble = DBBasic(BDouble);
	static var tbigint = DBBasic(BBigInt);
	static var tdecimal = DBBasic(BDecimal);
	static function tvarblob(s) return DBBasic(BVarBlob(s))
	static var tblob = DBBasic(BBlob);
	static function tunknown() return DBLazy({ref:null})
	
	var locals:Hash<QueryExpr>;
	var pos:Position;
	var sc:SolverContainer;
	var insideFunction:Bool;
	var currentBlock:Null<Array<QueryExpr>>;
	var ctx:time.compiler.Context;
	
	var hasReturn:Null<Position>; //just so we can check if the return is final
	var init:Bool;
	
	public function new(ctx)
	{
		insideFunction = false;
		locals = new Hash();
		this.ctx = ctx;
		this.sc = ctx.solvers;
	}
	
	public function parse(e:Expr) : ParsedQuery
	{
		//clearing entry point out, in case Parser is reused (TODO should we allow it?)
		locals = new Hash();
		init = false;
		
		var e = parseExpr(e);
		
		return {
			expr:e,
		};
	}
	
	
	function parseExpr(e:Expr) : QueryExpr
	{
		this.pos = e.pos;
		return switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CInt(v):
						queryExpr(QConst(QCInt(Std.parseInt(v))), tint);
					case CFloat(f):
						queryExpr(QConst(QCFloat(Std.parseFloat(f))), tdouble);
					case CString(s):
						queryExpr(QConst(QCString(s)), tvarchar(s.length));
					case CIdent(s), CType(s):
						switch(s)
						{
							case "null": queryExpr(QConst(QCNull), tunknown());
							case "true": queryExpr(QConst(QCBool(true)), tbool);
							case "false":queryExpr(QConst(QCBool(false)), tbool);
							default:
								if (!init)
								{

									//TODO support more complex QSelect exprs (like using another query)
									var dbtype = TypeBuilder.getData(s);

									init = true;
									queryExpr(QSelect(EPClass(s)), DBToMany(dbtype));
								} else if (locals.exists(s)) {
									queryExpr(QConst(QCVar(s)), lookupType(s));
								} else {
									queryExpr(QConst(QCExternalVar(s)), lookupType(s));
								}
						}
					default:
						unsupported();
						null;
				}
			case EArray(e1, e2): //redirect it to ECall to get - array access is an alias to get.
				parseExpr({expr:ECall({expr:EField(e1, "get"), pos:e1.pos}, [e2]), pos:e.pos});
			case EBinop(op, e1, e2):
				var e1db = parseExpr(e1);
				var e2db = parseExpr(e2);
				
				var me = this;
				function handleNumericOp(op, isBool = false)
				{
					var num = me.silent_numeric_unify(e1db.t, e2db.t);
					return if (num != null)
						me.queryExpr(QBinop(op, e1db, e2db), (isBool ? tbool : num));
					else
					{
						unify(e1db.t, e2db.t);
						
						parseExpr( { expr:ECall(
							{expr: EField(e1, StdType.enumConstructor(op)), pos:e1.pos}
							, [e2]), pos:e.pos});
						
						//me.queryExpr(QCall(me.change_type(e1db, u), StdType.enumConstructor(op), [me.change_type(e2db, u)]), (isBool ? tbool : u));
					}
				}
				
				switch(op)
				{
					case OpAdd:
						handleNumericOp(QOpAdd);
					case OpMult:
						handleNumericOp(QOpMult);
					case OpDiv:
						handleNumericOp(QOpDiv);
					case OpSub:
						handleNumericOp(QOpSub);
					case OpEq:
						unify(e1db.t, e2db.t);
						queryExpr(QBinop(QOpEq, e1db, e2db), tbool);
					case OpNotEq:
						unify(e1db.t, e2db.t); //just make sure types are compatible
						queryExpr(QBinop(QOpNotEq, e1db, e2db), tbool);
					case OpGt:
						handleNumericOp(QOpGt, true);
					case OpGte:
						handleNumericOp(QOpGte, true);
					case OpLt:
						handleNumericOp(QOpLt, true);
					case OpLte:
						handleNumericOp(QOpLte, true);
					case OpBoolAnd:
						queryExpr(QBinop(QOpBoolAnd, e1db, e2db), unify_bool(e1db.t, e2db.t));
					case OpBoolOr:
						queryExpr(QBinop(QOpBoolOr, e1db, e2db), unify_bool(e1db.t, e2db.t));
					default:
						unsupported();
						null;
				}
				
			case EUnop(op, _, e1):
				var e1db = parseExpr(e1);

				switch(op)
				{
					case OpNot:
						queryExpr(QUnop(QOpNot, e1db), unify_bool(e1db.t, tbool));
					case OpNeg:
						var rRate = rate(e1db.t);

						if (rRate >= 2 && rRate <= 5)
							queryExpr(QUnop(QOpNeg, e1db), e1db.t)
						else
						{
							parseExpr( { expr:ECall(
								{expr: EField(e1, StdType.enumConstructor(op)), pos:e1.pos},
								[]), pos:e.pos});
						}
					default:
						unsupported();
						null;
				}
			case EField(e1, f), EType(e1, f):
				{
				
					var e1db = parseExpr(e1);
					e1db.t = e1db.t.follow();
					
					var solvers = sc.getAll(e1db.t);
					if (solvers.length == 0)
					{
						error("No solver found for " + e1db.t.toString());
					}
					
					var ft = null;
					for (solver in solvers)
					{
						ft = solver.field(e1db, f);
						if (ft != null)
							break;
					}
					
					if (null == ft)
						error("Type " + e1db.t.toString() + " does not have field " + f);
					
					ft;
				}
			case EParenthesis(p):
				
				var pdb = parseExpr(p);
				queryExpr(QPar(pdb), pdb.t);
			case ECall(exp, params):
				switch(exp.expr)
				{
					case EField(e1, f), EType(e1, f):
						var e1db = parseExpr(e1);
						e1db.t = e1db.t.follow();
						
						var solvers = sc.getAll(e1db.t);
						if (solvers.length == 0)
						{
							error("No solver found for " + e1db.t.toString());
						}
						
						var i = 0;
						var ps = Lambda.array(Lambda.map(params, function(e) return parseParam(e1db.t, e, f, i++, solvers) ));
						var ft = null;
						
						for (solver in solvers)
						{
							ft = solver.call(e1db, f, ps);
							if (ft != null)
								break;
						}
						
						if (null == ft)
							error("Type " + e1db.t.toString() + " does not have field " + f);
						
						ft;
					case EConst(c):
						switch(c)
						{
							case CIdent(s): //might be a builtin call
								switch(s)
								{
									case "is":
										if (params.length != 2)
											error("incorrect number of parameters!");
										
										var v = parseExpr(params[0]);
										var type = classTypeExprToString(params[1]);
										
										//TODO insert here code to see if declared type and "is" type are compatible. Otherwise will always return false
										
										var type = queryExpr(QConst(QCType(type)), tvarchar(type.length));
										queryExpr(QBuiltin(BuiltinOp.BIs, [v, type]), tbool);
									case "enumName":
										if (params.length != 1)
											error("incorrect number of parameters!");
										
										var v = parseExpr(params[0]);
										queryExpr(QBuiltin(BuiltinOp.BEnumName, [v]), ttext);
/*									case "parent":
										if (params.length != 1)
											error("incorrect number of parameters!");
										
										var v = parseExpr(params[0]);
										queryExpr(QBuiltin(BuiltinOp.BParent, [v]), ttext); TODO support parent() construct*/
									case "cast":
										if (params.length != 2)
											error("incorrect number of parameters!");
										
										var v = parseExpr(params[0]);
										var type = classTypeExprToString(params[1]);
										
										var dbtype = TypeBuilder.getData(type);
										if (dbtype == null)
											error("Type not found: " + type);
										
										//cast will only work for compatible types
										unify(dbtype, v.t);
										
										var type = queryExpr(QConst(QCType(type)), tvarchar(type.length));
										queryExpr(QBuiltin(BuiltinOp.BCast, [v, type]), dbtype);
									case "query":
										if (params.length != 1)
											error("incorrect number of parameters!");
										var type = classTypeExprToString(params[0]);
										var dbtype = TypeBuilder.getData(type);
										
										init = true;
										
										queryExpr(QSelect(EPClass(type)), DBToMany(dbtype));
									default:
										error("Only field calls are allowed!");
								}
							default:
								error("Only field calls are allowed!");
						}
					default:
						error("Only field calls are allowed!");
				}
			case ENew(_, _), ETernary(_,_,_), EDisplayNew(_):
				unsupported();
			case EObjectDecl(_):
				//TODO add support for Object Declarations -> e.g. for comparison
				unsupported();
			case EArrayDecl(_):
				//TODO add support for Array Declarations -> e.g. for == comparison (deep comparison). See if possible on SQL
				unsupported();
			case EVars(vars):
				if (currentBlock == null) //null check just in case
					throw "assert. impossible variable declaration without block?";
				
				var last = null;
				for (v in vars) 
				{
					if (v.expr == null)
						error("All variables are immutable. It is illogical to not define them at creation time.");
					var e = parseExpr(v.expr);
					if (last != null) currentBlock.push(last);
					
					//TODO insert here code to see if ComplexType and actual type are compatible
					last = queryExpr(QVar(v.name, e), tvoid);
				}
				last;
			
			case EFunction(_, _):
				error("functions must only be declared inside calls.");
			
			case EBlock(b):
				var lastblock = this.currentBlock;
				var cblock = this.currentBlock = [];
				
				var last = b.length - 1;
				var i = 0;
				for (bl in b)
				{
					hasReturn = null;
					currentBlock.push(parseExpr(bl));
					if (hasReturn != null && i < last)
					{
						this.pos = hasReturn;
						error("All returns must be final.");
					}
					i++;
				}
				
				this.currentBlock = lastblock;
				
				queryExpr(QBlock(cblock), cblock[cblock.length - 1].t);
				
			case EFor(_, _, _), EIf(_,_,_), EWhile(_,_,_):
				unsupported();
			
			case ESwitch(_, _, _):
				//TODO support ESwitch so better enum matching is supported
				unsupported();
				
			case ETry(_,_): //no need for try/catch as queries will never throw any errors of their own. They will ignore null references and move on
				unsupported();
				
			case EReturn(ret):
				if (this.insideFunction)
				{
					hasReturn = this.pos; //this is made so we can check if return is final
					parseExpr(ret); //return is just a syntax "sugar". Won't make any difference at all. All returns must be final, so they are ommitted of the AST
				} else {
					error("Return must only be used inside a function block");
				}
			
			case EBreak, EContinue: unsupported();
			
			case EUntyped(e): //won't make a difference. Untyped expressions aren't allowed
				unsupported();
			
			case EThrow(_): //there is no kind of error handling inside queries.
				unsupported();
			
			case ECast(e, t):
				if ( t == null )
				{
					//queryExpr(parseExpr(e), tunknown());
					unsupported();
					//TODO support ECast with null t
				} else {
					switch(t)
					{
						case TPath(p):
							var p = p.pack.join(".") + (p.pack.length > 0 ? "." : "") + p.name;
							var t2 = TypeBuilder.getData(p);
							if (t2 == null)
							{
								error("Type not found: " + p);
							}
							
							var type = queryExpr(QConst(QCType(p)), tvarchar(p.length));
							queryExpr(QBuiltin(BuiltinOp.BCast, [parseExpr(e), type]), t2);
						default:
							//TODO support anonymous declarations
							unsupported();
					}
				}
			
			case EDisplay(e, isCall):
			//TODO change solver to support EDisplay
				unsupported();
				
		}
	}
	
	function classTypeExprToString(expr:Expr) : String
	{
		var type = Context.typeof(expr);
		return switch(type)
		{
			case TType(t, _):
				var t = t.get();
				if (t.name.charAt(0) != "#")
					error("Supplied argument must be a type name", expr.pos);
				t.pack.join(".") + (t.pack.length > 0 ? "." : "") + t.name.substr(1);
			default:
				error("Supplied argument must be a type name", expr.pos);
		};
	}
	
	
	//parse an ECall parameter. This will take care of parsing QFunction correctly, with all its short forms.
	//Also any other parameter-specific parsing will happen here.
	function parseParam(parentType:DBType, e:Expr, call:String, n:Int, solvers:Array<Solver>) : QueryExpr
	{
		return switch(e.expr)
		{
			case EFunction(_, fn):
				var args = [];
				
				var declared = [];
				var inferArgs = [];
				for (arg in fn.args)
				{
					var dbtype = null;
					
					var type = arg.type;
					if (type != null)
					{
						switch(type)
						{
							case TPath(p):
								//TODO: this way Arrays won't be supported
								dbtype = TypeBuilder.getData(p.pack.join(".") + (p.pack.length > 0 ? "." : "") + p.name);
								inferArgs.push(null);
							default:
								var ref = { ref:null };
								dbtype = DBLazy( ref );
								inferArgs.push(ref);
						}
					} else { 
						var ref = { ref:null };
						dbtype = DBLazy( ref );
						inferArgs.push(ref);
					}
					
					declared.push(declareVar(arg.name, { t:dbtype, pos:e.pos, expr:QBlock([]) })); //QBlock([]) => nop
					args.push( { name:arg.name, t:dbtype } );
				}
				
				for (solver in solvers)
				{
					if (solver.inferArgument(parentType, call, n, inferArgs))
						break;
				}
				
				var lastInside = this.insideFunction;
				insideFunction = true;
				
				var e = parseExpr(fn.expr);
				var ret = e.t;
				
				insideFunction = lastInside;
				
				for (i in 0...args.length)
				{
					if (declared[i] != null)
					{
						locals.set(args[i].name, declared[i]);
					} else {
						locals.remove(args[i].name);
					}
				}
				
				queryExpr(QFunction({
					args : args,
					ret : ret,
					expr : e
				}), tfunc);
			default:
				if (lookForSelfReferences(e))
				{
					var declared = declareVar("self", { t:parentType, pos:e.pos, expr:QBlock([]) } );
					var e2 = parseExpr(e);
					var ret = e2.t;
					
					if (declared == null)
						locals.remove("self");
					else
						locals.set("self", declared);
					
					queryExpr(QFunction({
						args : [{name:"self", t:parentType}],
						ret : ret,
						expr : e2
					}), tfunc);
				} else {
					parseExpr(e);
				}
		}
	}
	 
	function declareVar(name:String, expr:QueryExpr) : Null<QueryExpr>
	{
		var lastLocal = locals.get(name);
		
		locals.set(name, expr);
		return lastLocal;
	}
	
	function lookForSelfReferences(e:Expr) : Bool
	{
		
		var found = false;
		function hasSelf(e:Expr) : Expr
		{
			if (found)
				return null;
			return switch(e.expr)
			{
				case EConst(c):
					switch(c)
					{
						case CIdent(i):
							found = i == "self";
							null;
						default:
							null;
					}
				case EFunction(_, _): //stop if another EFunction is found
					null;
				case ECall(_, _):
					null;
				default:
					Map.mapExpr(hasSelf, e);
			}
		}
		
		hasSelf(e);
		return found;
	}
	
	
	function change_type(expr:QueryExpr, t:DBType):QueryExpr
	{
		return { t:t, pos:expr.pos, expr:expr.expr };
	}
	
	function unify_bool(t1:DBType, t2:DBType) : DBType
	{
		var r = unify(t1, t2);
		var rRate = rate(r);
		
		if (rRate == 6)
			return r;
		
		error("Type " + r.toString() + " is not Bool!");
		return null;
	}
	
	function unify_str_numeric(t1:DBType, t2:DBType) :DBType
	{
		var r = unify(t1, t2);
		var rRate = rate(r);
		
		if (rRate >= 2 && rRate <= 5 || rRate == 0)
			return r;
		
		error("Type " + r.toString() + " is not numeric nor string!");
		return null;
	}
	
	function silent_numeric_unify(t1:DBType, t2:DBType) : Null<DBType>
	{
		var r = unify(t1, t2);
		var rRate = rate(r);
		
		if (rRate >= 2 && rRate <= 5)
			return r;
		
		return null;
	}
	
	function numeric_unify(t1:DBType, t2:DBType) :DBType
	{
		var r = unify(t1, t2);
		var rRate = rate(r);
		
		if (rRate >= 2 && rRate <= 5)
			return r;
		
		error("Type " + r.toString() + " is not numeric!");
		return null;
	}
	
	public static function unify(t1:DBType, t2:DBType) : DBType
	{
		function unify_error ()  throw ("cannot unify " + t1.toString() + " and " + t2.toString()); //TODO add global error handler
		
		//support for unifying unknown types - will infer to be the same as unify() tryout
		function setUnknown(type, to)
		{
			switch(type)
			{
				case DBLazy(ref):
					if (ref.ref == null)
						ref.ref = to;
					else
						setUnknown(ref.ref, to);
				default:
					throw "assert " + type.toString();
			}
		}
		
		var r = rate(t1);
		if (r != null)
		{
			if (r == -1)
			{
				var r2 = rate(t2);
				if (r2 == -1)
					throw ("Illogical (?): both unify parts are Unknown"); //TODO verify if it really is illogical
				
				setUnknown(t1, t2);
				return t2;
			}
			
			var r2 = rate(t2);
			if (r2 == -1)
			{
				setUnknown(t2, t1);
				return t1;
			}
			
			function isNumeric(r) return r >= 2 && r <= 5;
			if (r2 == null)
			{
				unify_error();
				return null;
			}
			else if (r == r2)
			{
				return t1; //both are the same
			}
			else if (isNumeric(r) && isNumeric(r2))
			{
				return if (r > r2) t1; else t2;
			} else if (r == 0 || r2 == 0) //string conversion
			{
				return ttext;
			} else {
				unify_error();
				return null;
			}
		} else {
			var r2 = rate(t2);
			if (r2 == -1)
			{
				setUnknown(t2, t1);
				return t1;
			}
			
			if (t1.toString() == t2.toString())
				return t1;
			
			var t1_path = new Hash();
			
			var t1_temp = t1;
			while (t1_temp != null)
			{
				switch(t1_temp)
				{
					case DBInst(inst):
						if (inst == null)
							break;
						
						t1_path.set(inst.path, inst);
						t1_temp = DBInst(inst.superClass);
					default:
						unify_error();
				}
			}
			
			var t2_temp = t2;
			while (t2_temp != null)
			{
				switch(t2_temp)
				{
					case DBInst(inst):
						if (inst == null)
							break;
						
						if (t1_path.exists(inst.path))
							return DBInst(inst); //minimal common type
						t2_temp = DBInst(inst.superClass);
					default:
						unify_error();
				}
			}
			
			unify_error();
			return null;
		}
	}
	
	static function rate(t:DBType) : Null<Int>
	{
		return switch(t)
		{
			case DBBasic(b):
				switch(b)
				{
					case BVarchar(_): 0;
					case BText: 0;
					case BDate: 1;
					case BInt: 2;
					case BInt32: 3;
					case BInt64: 4;
					case BDouble: 5;
					case BBool: 6;
					case BBigInt: 7;
					case BDecimal: 8;
					case BVarBlob(size): 9;
					case BBlob: 9;
					case BVoid: 10;
					case BFunction: 11;
					case BKey(_): 12;
					case BSerializedType: 13;
				}
			case DBLazy(ref):
				if (ref.ref == null) //Unknown
					-1;
				else
					rate(ref.ref);
			default:
				null;
		}
	}
	
	
	function lookupType(s) : DBType
	{
		var t = this.locals.get(s);
		if (t != null)
		{
			return t.t;
		}
		else
		{
			var expr = if (s.charAt(0) == s.charAt(0).toUpperCase()) EConst(CType(s)); else EConst(CIdent(s));
			var t = Context.typeof( { expr:expr, pos:this.pos } );
			return TypeBuilder.convertToDb(t);
		}
	}
	
	
	function queryExpr(expr:QueryExprDef, t:DBType) : QueryExpr
	{
		return {expr:expr, t:t, pos:this.pos};
	}
	
	
	function mk(e:ExprDef) : Expr
	{
		return {expr:e, pos:this.pos};
	}
	
	
	function extractType(e:Expr) : Type
	{
		var offending = null;
		var path = new StringBuf();
		function check(e)
		{
			return switch(e.expr)
			{
				case EConst(c):
					switch(c)
					{
						case CIdent(n):
							path.add(n);
							true;
						default:
							offending = e;
							false;
					}
				case EField(e, f):
					var r = check(e);
					
					path.add(".");
					path.add(f);
					
					r;
				case EType(e, f):
					var r = check(e);
					
					path.add(".");
					path.add(f);
					
					r;
				default:
					offending = e;
					false;
			}
		}
		
		if (!check(e))
		{
			this.pos = e.pos;
			error("Unexpected " + offending + ". Expecting type path.");
		}
		
		return Context.getType(path.toString());
	}
	
	
	function error(msg:String, ?p) : Dynamic
	{
		if (p == null)
			p = pos;
		Context.error(msg, p);
		throw msg;
	}
	
	function unsupported() : Dynamic
	{
		error("Unsupported operation.");
		return null;
	}
	
	#else
	
	public function new() : Void
	{
		
	}
	
	#end
	
}

#end